/**
 * WTF Engine
 * 
 * License... etc.
 **
 * Water Effects Shader
 **
 * Authors: Sean Micklethwaite
 */

#include "fx_water_surface.h"
#include "pmodel.h"
#include "fx_reflection.h"

using namespace WtfEngine;
using namespace WtfGraphics;

/// Number of texture cells per game-metre
#define WATER_SURFACE_RESOLUTION	1.0
#define WATER_SURFACE_DAMPING		0.02
#define WATER_SURFACE_CORRECTION	0.001
/// Excitement frequency
#define WATER_SURFACE_EXC_FREQ		0.1

#define WATER_SURFACE_RIPPLE		2.0


/**** Water Surface Refraction Shader ****/

WaterSurfaceRefractionShader::WaterSurfaceRefractionShader(const CubeMap::tRef& rCubeMap):
	GLSLProgram(), mrCubeMap(rCubeMap)
{
	GLSLShader::tRef	rFrag(new GLSLShader());
	GLSLShader::tRef	rVert(new GLSLShader());

	rFrag->Load("src/shaders/water_surface_refraction.frag");
	rVert->Load("src/shaders/water_surface_refraction.vert");

	this->Load(rFrag, rVert);
};

void WaterSurfaceRefractionShader::GetLocations() {
	mCubeMapLocation = glGetUniformLocationARB(hndProgram, "gCubeMap");
	mSurfaceLocation = glGetUniformLocationARB(hndProgram, "gSurface");
};

void WaterSurfaceRefractionShader::PassOther() {
	// Pass location of height map texture
	glUniform1iARB(mCubeMapLocation, 1);
	checkGLError("passing cube map texture handle");

	glUniform1iARB(mSurfaceLocation, 0);

	mrCubeMap->Bind(1);
};

void WaterSurfaceRefractionShader::Deactivate() {
	mrCubeMap->Release();
	GLSLProgram::Deactivate();
};


/**
 * This shader is used by water surfaces when using a realtime
 * 2D reflection map, which is projected onto the surface.
 **/
class WaterSurfaceProjectionShader: public GLSLProgram {
private:
	int	mReflectionMapLocation, mSurfaceLocation;
	Texture::tRef mrReflectionMap;

public:
	void GetLocations() {
		mReflectionMapLocation = glGetUniformLocationARB(hndProgram, "gReflectionMap");
		mSurfaceLocation = glGetUniformLocationARB(hndProgram, "gSurface");
	};

	void PassOther() {
		// Pass location of height map texture
		glUniform1iARB(mReflectionMapLocation, 1);
		checkGLError("passing reflection map texture handle");

		glUniform1iARB(mSurfaceLocation, 0);

		mrReflectionMap->Bind(1);
	};

	void Deactivate() {
		mrReflectionMap->Release();
		GLSLProgram::Deactivate();
	};

public:
	WaterSurfaceProjectionShader(const Texture::tRef& rReflectionMap)
	: GLSLProgram(), mrReflectionMap(rReflectionMap)
	{
		GLSLShader::tRef	rFrag(new GLSLShader());
		GLSLShader::tRef	rVert(new GLSLShader());

		rFrag->Load("src/shaders/water_surface_projection.frag");
		rVert->Load("src/shaders/water_surface_projection.vert");

		this->Load(rFrag, rVert);
	};
	
	GC_AUTOSIZE(WaterSurfaceProjectionShader);
};


/**** Water Surface ****/

WaterSurface::WaterSurface(const WaterSurface::tCreationOptions& args, const tVector& vDim, const fVector2D& vCurrent):
	Static(args, new TerrainHeightMap("water surface"), new PhysicalModels::Oblong("", vDim)), mvCurrent(vCurrent) {
	
	mrHeightMap = dynamic_cast<TerrainHeightMap*>(&*getGraphicsData());
	mrHeightMap->setTexture(DataManager::GetInstance().Load<TextureImage>("media/textures/water512.jpg"));

	if(false) {
		mrHeightMap->setShader(new WaterSurfaceRefractionShader(DataManager::GetInstance().Load<CubeMap>("media/cubemaps/test")));
	} else {
		mrHeightMap->setShader(new WaterSurfaceProjectionShader(
			new ObliqueFrustumReflection(tPosition(), tPlane(tDirection(0, 1, 0), 0))));
	};

	// Bind event handler
	Kernel::GetInstance().GetGameState()->BindEvent(new ObjectEvent(this), new ObjFunctor<WaterSurface>(this, &WaterSurface::EventHandler));

	if(false) {
		// Initialise FBO heightmap buffers
		
	} else {
		int			i;
		float		d;
		mvDim[0] = (int)(WATER_SURFACE_RESOLUTION * vDim[0]);
		mvDim[1] = (int)(WATER_SURFACE_RESOLUTION * vDim[2]);
		unsigned	uSize = mvDim[0] * mvDim[1];
		
		// Initialise heightmap buffers
		mprHeightMapA = new NonCopyBuffer<tScalar>::tRef(new NonCopyBuffer<tScalar>(uSize));
		mprHeightMapB = new NonCopyBuffer<tScalar>::tRef(new NonCopyBuffer<tScalar>(uSize));
		mpVelocities = new tScalar[uSize];

		// Initialise surfaces
		tScalar * pHeightMapA((*mprHeightMapA)->getData()), * pHeightMapB((*mprHeightMapB)->getData());
		for(i = 0; i < uSize; i++) {
			pHeightMapA[i] = pHeightMapB[i] = mpVelocities[i] = 0;
		};
	};

	IDynamic::Init();
};

WaterSurface::~WaterSurface() {
	if(false) {

	} else {
		delete mprHeightMapA;
		delete mprHeightMapB;
		delete mpVelocities;
	};
};


void WaterSurface::EventHandler() {
	ObjectCollisionEvent * pEv = Kernel::GetInstance().GetCurrentEvent<ObjectCollisionEvent>();
	if(pEv) {
		// Collision with surface - create a ripple at the contact point
		Physical *	pObject;
		tVector		vRelPos;
		int			pos;
		tScalar		amt;

		pObject = pEv->getInteractingObject<Physical>();
		if(!pObject) return;

		// Relative position from centre of water, add half dimensions to
		// get the position from corner.
		vRelPos = pObject->getPosition() - getPosition();
		vRelPos[0] += mvDim[0] / 2;
		vRelPos[2] += mvDim[1] / 2;
		
		pos = ((int)vRelPos[0]) + (((int)vRelPos[2]) * mvDim[0]);

		// Size of the ripple & splash is directly proportional to momentum of the object
		amt = sqrt(pObject->getVelocity().length() / pObject->getMassQuot());
		
		if(pos >= 0 && pos < (mvDim[0] * mvDim[1]))
			(**mprHeightMapA)[pos] -= amt * WATER_SURFACE_RIPPLE;
		
		// Set to processed to prevent bounce - buoyancy is not handled here.
		Kernel::GetInstance().SetEventStatus(Event::PROCESSED);
	};
};

void WaterSurface::Update() {
	static tGameTime t = 0;
	tGameTime dt = Kernel::GetInstance().GetGameState()->GetDelta();
	t += dt;

	if(false) {
		
	} else {
		// Run water simulation on CPU
		int i, y;
		NonCopyBuffer<tScalar>::tRef * prTmp;
		tScalar * pHeightMapA((*mprHeightMapA)->getData()), * pHeightMapB((*mprHeightMapB)->getData());

		// "Excite" the edges
		for(i = 0; i < mvDim[0]; i++)
			pHeightMapA[i] = pHeightMapB[i] = sin((tScalar)i + (t / WATER_SURFACE_EXC_FREQ)) * 100;
		for(i = 0; i < mvDim[0] * mvDim[1]; i += mvDim[0])
			pHeightMapA[i] = pHeightMapB[i] = sin((tScalar)i + (t / WATER_SURFACE_EXC_FREQ)) * 100;
		for(i = mvDim[0] - 1; i < mvDim[0] * mvDim[1]; i += mvDim[0])
			pHeightMapA[i] = pHeightMapB[i] = sin((tScalar)i + (t / WATER_SURFACE_EXC_FREQ)) * 100;
		for(i = mvDim[0] * (mvDim[1] - 1); i < mvDim[0] * mvDim[1]; i++)
			pHeightMapA[i] = pHeightMapB[i] = sin((tScalar)i + (t / WATER_SURFACE_EXC_FREQ)) * 100;

		for(y = 1; y < mvDim[1] - 1; y++) for(i = (mvDim[0] * y) + 1; i < (mvDim[0] * (y + 1)) - 1; i++) {
			mpVelocities[i] += (
					 ((pHeightMapA[i - 1] * (1 + mvCurrent[0])) +
                      (pHeightMapA[i + 1] * (1 - mvCurrent[0])) +
                      (pHeightMapA[i - mvDim[0]] * (1 + mvCurrent[1])) +
                      (pHeightMapA[i + mvDim[0]] * (1 - mvCurrent[1]))) - (pHeightMapA[i] * 4)) * (dt / 0.001);

			// Main damping
			mpVelocities[i] *= (1.0 - WATER_SURFACE_DAMPING);

			// Update height
			pHeightMapB[i] = pHeightMapA[i] + (mpVelocities[i] * dt);
			
			// Exponential damping - stop things getting out of hand
			pHeightMapB[i] *= exp(-abs(pHeightMapB[i]) * WATER_SURFACE_CORRECTION);
		};

		// Swap buffers
		prTmp = mprHeightMapA;
		mprHeightMapA = mprHeightMapB;
		mprHeightMapB = prTmp;

		// Update heightmap
		mrHeightMap->Create(tVector(1 / WATER_SURFACE_RESOLUTION, 0.005, 1 / WATER_SURFACE_RESOLUTION), mvDim,
			*mprHeightMapA, TerrainHeightMap::HTMAP_FLOATV);
	};
};
